Problem 2 - Generative Adversarial Networks (GAN)¶

  • Learning Objective: In this problem, you will implement a Generative Adversarial Network with the network structure proposed in Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks. You will also learn a visualization technique: activation maximization.
  • Provided code: The code for constructing the two parts of the GAN, the discriminator and the generator, is done for you, along with the skeleton code for the training.
  • TODOs: You will need to figure out how to define the training loop, compute the loss, and update the parameters to complete the training and visualization. In addition, to test your understanding, you will answer some non-coding written questions. Please see details below.

Note:

  • If you use the Colab, for faster training of the models in this assignment, you can enable GPU support in the Colab. Navigate to "Runtime" --> "Change Runtime Type" and set the "Hardware Accelerator" to "GPU". However, Colab has the GPU limit, so be discretional with your GPU usage.
  • If you run into CUDA errors in the Colab, check your code carefully. After fixing your code, if the CUDA error shows up at a previously correct line, restart the Colab. However, this is not a fix to all your CUDA issues. Please check your implementation carefully.
In [ ]:
# Import required libraries
import torch.nn as nn
import torch
import numpy as np
import matplotlib.pyplot as plt
import math
from torchvision.utils import make_grid
%matplotlib inline

# for auto-reloading external modules
# see http://stackoverflow.com/questions/1907993/autoreload-of-modules-in-ipython
%load_ext autoreload
%autoreload 2

Introduction: The forger versus the police¶

Please read the information below even if you are familiar with GANs. There are some terms below that will be used in the coding part.

Generative models try to model the distribution of the data in an explicit way, in the sense that we can easily sample new data points from this model. This is in contrast to discriminative models that try to infer the output from the input. In class and in the previous problem, we have seen one classic deep generative model, the Variational Autoencoder (VAE). Here, we will learn another generative model that has risen to prominence in recent years, the Generative Adversarial Network (GAN).

As the math of Generative Adversarial Networks are somewhat tedious, a story is often told of a forger and a police officer to illustrate the idea.

Imagine a forger that makes fake bills, and a police officer that tries to find these forgeries. If the forger were a VAE, his goal would be to take some real bills, and try to replicate the real bills as precisely as possible. With GANs, the forger has a different idea: rather than trying to replicate the real bills, it suffices to make fake bills such that people think they are real.

Now let's start. In the beginning, the police knows nothing about how to distinguish between real and fake bills. The forger knows nothing either and only produces white paper.

In the first round, the police gets the fake bill and learns that the forgeries are white while the real bills are green. The forger then finds out that white papers can no longer fool the police and starts to produce green papers.

In the second round, the police learns that real bills have denominations printed on them while the forgeries do not. The forger then finds out that plain papers can no longer fool the police and starts to print numbers on them.

In the third round, the police learns that real bills have watermarks on them while the forgeries do not. The forger then has to reproduce the watermarks on his fake bills.

...

Finally, the police is able to spot the tiniest difference between real and fake bills and the forger has to make perfect replicas of real bills to fool the police.

Now in a GAN, the forger becomes the generator and the police becomes the discriminator. The discriminator is a binary classifier with the two classes being "taken from the real data" ("real") and "generated by the generator" ("fake"). Its objective is to minimize the classification loss. The generator's objective is to generate samples so that the discriminator misclassifies them as real.

Here we have some complications: the goal is not to find one perfect fake sample. Such a sample will not actually fool the discriminator: if the forger makes hundreds of the exact same fake bill, they will all have the same serial number and the police will soon find out that they are fake. Instead, we want the generator to be able to generate a variety of fake samples such that when presented as a distribution alongside the distribution of real samples, these two are indistinguishable by the discriminator.

So how do we generate different samples with a deterministic generator? We provide it with random numbers as input.

Typically, for the discriminator we use binary cross entropy loss with label 1 being real and 0 being fake. For the generator, the input is a random vector drawn from a standard normal distribution. Denote the generator by $G_\phi(z)$, discriminator by $D_\theta (x)$, the distribution of the real samples by $p(x)$, and the input distribution to the generator by $q(z)$. Recall that the binary cross entropy loss with classifier output $y$ and label $\hat{y}$ is

$$L(y, \hat{y}) = -\hat{y} \log y - (1 - \hat{y}) \log (1 - y)$$

For the discriminator, the objective is $$\min_{\theta} \mathrm{E}_{x \sim p(x)}[L(D_{\theta}(x), 1)] + \mathrm{E}_{z \sim q(z)}[L(D_{\theta}(G_{\phi}(z)), 0)]$$

For the generator, the objective is $$\max_{\phi} \mathrm{E}_{z \sim q(z)}[L(D_{\theta}(G_{\phi}(z)), 0)]$$

The generator's objective corresponds to maximizing the classification loss of the discriminator on the generated samples. Alternatively, we can minimize the classification loss of the discriminator on the generated samples when labelled as real: $$\min_{\phi} \mathrm{E}_{z \sim q(z)}[L(D_{\theta}(G_{\phi}(z)), 1)]$$

And this is what we will use in our implementation. The strength of the two networks should be balanced, so we train the two networks alternatingly, updating the parameters in both networks once in each iteration.

Problem 2-1: Implementing the GAN (20 pts)¶

Correctly filling out __init__: 7 pts

Correctly filling out training loop: 13 pts

We first load the data (CIFAR-10) and define some convenient functions. You can run the cell below to download the dataset to ./data.

In [ ]:
#  !wget http://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz -P data
#  !tar -xzvf data/cifar-10-python.tar.gz --directory data
#  !rm data/cifar-10-python.tar.gz
In [ ]:
def unpickle(file):
    import sys
    if sys.version_info.major == 2:
        import cPickle
        with open(file, 'rb') as fo:
            dict = cPickle.load(fo)
        return dict['data'], dict['labels']
    else:
        import pickle
        with open(file, 'rb') as fo:
            dict = pickle.load(fo, encoding='bytes')
        return dict[b'data'], dict[b'labels']
def load_train_data():
    X = []
    for i in range(5):
        X_, _ = unpickle('data/cifar-10-batches-py/data_batch_%d' % (i + 1))
        X.append(X_)
    X = np.concatenate(X)
    X = X.reshape((X.shape[0], 3, 32, 32))
    return X

def load_test_data():
    X_, _ = unpickle('data/cifar-10-batches-py/test_batch')
    X = X_.reshape((X_.shape[0], 3, 32, 32))
    return X

def set_seed(seed):
    np.random.seed(seed)
    torch.manual_seed(seed)

# Load cifar-10 data
train_samples = load_train_data() / 255.0
test_samples= load_test_data() / 255.0

To save you some mundane work, we have defined a discriminator and a generator for you. Look at the code to see what layers are there.

For this part, you need to complete code blocks marked with "Prob 2-1":¶

  • Build the Discriminator and Generator, define the loss objectives
  • Define the optimizers
  • Build the training loop and compute the losses: As per How to Train a GAN? Tips and tricks to make GANs work, we put real samples and fake samples in different batches when training the discriminator.

Note: use the advice on that page with caution if you are using GANs for your team project. It is already 4 years old, which is a really long time in deep learning research. It does not reflect the latest results.

In [ ]:
class Generator(nn.Module):
    def __init__(self, starting_shape):
        super(Generator, self).__init__()
        self.fc = nn.Linear(starting_shape, 4 * 4 * 128)
        self.upsample_and_generate = nn.Sequential(
            nn.BatchNorm2d(128),
            nn.LeakyReLU(),
            nn.ConvTranspose2d(in_channels=128, out_channels=64, kernel_size=4, stride=2, padding=1, bias=True),
            nn.BatchNorm2d(64),
            nn.LeakyReLU(),
            nn.ConvTranspose2d(in_channels=64, out_channels=32, kernel_size=4, stride=2, padding=1, bias=True),
            nn.BatchNorm2d(32),
            nn.LeakyReLU(),
            nn.ConvTranspose2d(in_channels=32, out_channels=3, kernel_size=4, stride=2, padding=1, bias=True),
            nn.Sigmoid()
        )
    def forward(self, input):
        transformed_random_noise = self.fc(input)
        reshaped_to_image = transformed_random_noise.reshape((-1, 128, 4, 4))
        generated_image = self.upsample_and_generate(reshaped_to_image)
        return generated_image
In [ ]:
class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        self.downsample = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=32, kernel_size=4, stride=2, padding=1, bias=True),
            nn.LeakyReLU(),
            nn.Conv2d(in_channels=32, out_channels=64, kernel_size=4, stride=2, padding=1, bias=True),
            nn.BatchNorm2d(64),
            nn.LeakyReLU(),
            nn.Conv2d(in_channels=64, out_channels=128, kernel_size=4, stride=2, padding=1, bias=True),
            nn.BatchNorm2d(128),
            nn.LeakyReLU(),
        )
        self.fc = nn.Linear(4 * 4 * 128, 1)
    def forward(self, input):
        downsampled_image = self.downsample(input)
        reshaped_for_fc = downsampled_image.reshape((-1, 4 * 4 * 128))
        classification_probs = self.fc(reshaped_for_fc)
        return classification_probs
In [ ]:
# Use this to put tensors on GPU/CPU automatically when defining tensors
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu') 
print(device)
class DCGAN(nn.Module):
    def __init__(self):
        super(DCGAN, self).__init__()
        self.num_epoch = 25
        self.batch_size = 128
        self.log_step = 100 
        self.visualize_step = 2 
        self.code_size = 64  # size of latent vector (size of generator input)
        self.learning_rate = 2e-4
        self.vis_learning_rate = 1e-2
        
        # IID N(0, 1) Sample
        self.tracked_noise = torch.randn([64, self.code_size], device=device)        
        
        self._actmax_label = torch.ones([64, 1], device=device)
        
        ################################################################################
        # Prob 2-1: Define the generator and discriminator, and loss functions  #
        # Also, apply the custom weight initialization (see link:                      #
        # https://pytorch.org/tutorials/beginner/dcgan_faces_tutorial.html)            #
        ################################################################################
        # To-Do: Initialize generator and discriminator
        # use variable name "self._generator" and "self._discriminator", respectively
        # (also move them to torch device for accelerating the training later)
        self._generator = Generator(self.code_size).to(device)
        self._discriminator = Discriminator().to(device)


        # To-Do: Apply weight initialization (first implement the weight initialization 
        # function below by following the given link)
        self._generator.apply(self._weight_initialization)
        self._discriminator.apply(self._weight_initialization)


        ################################################################################
        # Prob 2-1: Define the generator and discriminators' optimizers                #
        # HINT: Use Adam, and the provided momentum values (betas)                     #
        ################################################################################
        betas = (0.5, 0.999)
        # To-Do: Initialize the generator's and discriminator's optimizers 
        self.gen_optimizer = torch.optim.Adam(self._generator.parameters(), lr=self.learning_rate, betas=betas)
        self.dis_optimizer = torch.optim.Adam(self._discriminator.parameters(), lr=self.learning_rate, betas=betas)



    # To-Do: Define weight initialization function
    # see link: https://pytorch.org/tutorials/beginner/dcgan_faces_tutorial.html
    def _weight_initialization(self, m):
      classname = m.__class__.__name__
      if classname.find('Conv') != -1:
          nn.init.normal_(m.weight.data, 0.0, 0.02)
      elif classname.find('BatchNorm') != -1:
          nn.init.normal_(m.weight.data, 1.0, 0.02)
          nn.init.constant_(m.bias.data, 0)


    # To-Do: Define a general classification loss function (sigmoid followed by binary cross entropy loss)
    def _classification_loss(self, output, target):
      return nn.BCEWithLogitsLoss()(output, target)


        ################################################################################
        #                               END OF YOUR CODE                               #
        ################################################################################

    # Training function
    def train(self, train_samples):
        num_train = train_samples.shape[0]
        step = 0
        
        # smooth the loss curve so that it does not fluctuate too much
        smooth_factor = 0.95
        plot_dis_s = 0
        plot_gen_s = 0
        plot_ws = 0
        
        dis_losses = []
        gen_losses = []
        max_steps = int(self.num_epoch * (num_train // self.batch_size))
        fake_label = torch.zeros([self.batch_size, 1], device=device)
        real_label = torch.ones([self.batch_size, 1], device=device)
        self._generator.train()
        self._discriminator.train()
        print('Start training ...')
        for epoch in range(self.num_epoch):
            np.random.shuffle(train_samples)
            for i in range(num_train // self.batch_size):
                step += 1

                batch_samples = train_samples[i * self.batch_size : (i + 1) * self.batch_size]
                batch_samples = torch.Tensor(batch_samples).to(device)

                ################################################################################
                # Prob 2-1: Train the discriminator on all real images first            #
                ################################################################################
                # To-Do: HINT: Remember to eliminate all discriminator gradients first! (.zero_grad())
                self._discriminator.zero_grad()

                # To-Do: feed real samples to the discriminator
                real_dis_output = self._discriminator(batch_samples)

                # To-Do: calculate the discriminator loss for real samples
                # use the variable name "real_dis_loss"
                real_dis_loss = self._classification_loss(real_dis_output, real_label)


                ################################################################################
                # Prob 2-1: Train the discriminator with an all fake batch                     #
                ################################################################################   
                # To-Do: sample noises from IID Normal(0, 1)^d on the torch device
                noise = torch.randn(self.batch_size, self.code_size, device=device)

                # To-Do: generate fake samples from the noise using the generator
                fake_samples = self._generator(noise).detach()


                # To-Do: feed fake samples to discriminator
                # Make sure to detach the fake samples from the gradient calculation 
                # when feeding to the discriminator, we don't want the discriminator to 
                # receive gradient info from the Generator
                fake_dis_output = self._discriminator(fake_samples)


                # To-Do: calculate the discriminator loss for fake samples
                # use the variable name "fake_dis_loss"
                fake_dis_loss = self._classification_loss(fake_dis_output, fake_label)


                # To-Do: calculate the total discriminator loss (real loss + fake loss)
                dis_loss = real_dis_loss + fake_dis_loss

                
                # To-Do: calculate the gradients for the total discriminator loss
                dis_loss.backward()


                # To-Do: update the discriminator weights
                self.dis_optimizer.step()
                
                
                
                ################################################################################
                # Prob 2-1: Train the generator                                                #
                ################################################################################                
                # To-Do: Remember to eliminate all generator gradients first! (.zero_grad())
                self._generator.zero_grad()
    
                # To-Do: sample noises from IID Normal(0, 1)^d on the torch device
                noise = torch.randn(self.batch_size, self.code_size, device=device)


                # To-Do: generate fake samples from the noise using the generator
                fake_samples = self._generator(noise)


                # To-Do: feed fake samples to the discriminator
                # No need to detach from gradient calculation here, we want the 
                # generator to receive gradient info from the discriminator 
                # so it can learn better.
                gen_dis_output = self._discriminator(fake_samples)
 

                # To-Do: calculate the generator loss 
                # hint: the goal of the generator is to make the discriminator 
                # consider the fake samples as real
                gen_loss = self._classification_loss(gen_dis_output, real_label)

                # To-Do: Calculate the generator loss gradients
                gen_loss.backward()

                # To-Do: Update the generator weights
                self.gen_optimizer.step()

                
                
                ################################################################################
                #                               END OF YOUR CODE                               #
                ################################################################################

                dis_loss = real_dis_loss + fake_dis_loss
                
                plot_dis_s = plot_dis_s * smooth_factor + dis_loss * (1 - smooth_factor)
                plot_gen_s = plot_gen_s * smooth_factor + gen_loss * (1 - smooth_factor)
                plot_ws = plot_ws * smooth_factor + (1 - smooth_factor)
                dis_losses.append(plot_dis_s / plot_ws)
                gen_losses.append(plot_gen_s / plot_ws)

                if step % self.log_step == 0:
                    print('Iteration {0}/{1}: dis loss = {2:.4f}, gen loss = {3:.4f}'.format(step, max_steps, dis_loss, gen_loss))

            if epoch % self.visualize_step == 0:
                fig = plt.figure(figsize = (8, 8))   
                ax1 = plt.subplot(111)
                ax1.imshow(make_grid(self._generator(self.tracked_noise.detach()).cpu().detach(), padding=1, normalize=True).numpy().transpose((1, 2, 0)))
                plt.show()

                dis_losses_cpu = [_.cpu().detach() for _ in dis_losses]
                plt.plot(dis_losses_cpu)
                plt.title('discriminator loss')
                plt.xlabel('iterations')
                plt.ylabel('loss')
                plt.show()
    
                gen_losses_cpu = [_.cpu().detach() for _ in gen_losses]
                plt.plot(gen_losses_cpu)
                plt.title('generator loss')
                plt.xlabel('iterations')
                plt.ylabel('loss')
                plt.show()
        print('... Done!')



    #########################################################
    # Prob 2-4: Find the reconstruction of a batch of samples
    # **skip this part when working on problem 2-1 and come back for problem 2-4 
    ####################################################################
    # Prob 2-4: To-Do: Define squared L2-distance function (or Mean-Squared-Error) 
    # as reconstruction loss
    ####################################################################
    def _reconstruction_loss(self, original, reconstruction):
        mse_loss = nn.MSELoss()
        loss = mse_loss(original, reconstruction)
        return loss



    def reconstruct(self, samples):
        recon_code = torch.zeros([samples.shape[0], self.code_size], device=device, requires_grad=True)
        samples = torch.tensor(samples, device=device, dtype=torch.float32)

        # Set the generator to evaluation mode, to make batchnorm stats stay fixed
        self._generator.eval()

        ################################################################################
        # Prob 2-4: complete the definition of the optimizer.                          #
        # **skip this part when working on problem 2-1 and come back for problem 2-4   #
        ################################################################################       
        # To-Do: define the optimizer
        # Hinit: Use self.vis_learning_rate as one of the parameters for Adam optimizer
        optimizer = torch.optim.Adam([recon_code], lr=self.vis_learning_rate)
 
        
        for i in range(500):
            ################################################################################
            # Prob 2-4: Fill in the training loop for reconstruciton                       #
            # **skip this part when working on problem 2-1 and come back for problem 2-4   #
            ################################################################################
            # To-Do: eliminate the gradients
            optimizer.zero_grad()

            # To-Do: feed the reconstruction codes to the generator for generating reconstructed samples 
            # use the variable name "recon_samples"
            # recon_samples = [] # comment out this line when you are coding
            recon_samples = self._generator(recon_code)

            # To-Do: calculate reconstruction loss
            # use the variable name "recon_loss"
            # recon_loss = 0.0 # comment out this line when you are coding
            recon_loss = self._reconstruction_loss(samples, recon_samples)

            # To-Do: calculate the gradient of the reconstruction loss 
            recon_loss.backward()

            # To-Do: update the weights
            optimizer.step()

        
            ################################################################################
            #                               END OF YOUR CODE                               #
            ################################################################################
            
        return recon_loss, recon_samples.detach().cpu()
        

    # Perform activation maximization on a batch of different initial codes
    def actmax(self, actmax_code):
        self._generator.eval()
        self._discriminator.eval() 
        ################################################################################
        # Prob 2-4: just check this function. You do not need to code here             #
        # skip this part when working on problem 2-1 and come back for problem 2-4     #
        ################################################################################
        actmax_code = torch.tensor(actmax_code, device=device, dtype=torch.float32, requires_grad=True)
        actmax_optimizer = torch.optim.Adam([actmax_code], lr=self.vis_learning_rate) 
        for i in range(500):
            actmax_optimizer.zero_grad()
            actmax_sample = self._generator(actmax_code)
            actmax_dis = self._discriminator(actmax_sample)
            actmax_loss = self._classification_loss(actmax_dis, self._actmax_label)
            actmax_loss.backward()
            actmax_optimizer.step()
        return actmax_sample.detach().cpu()
cpu

Now let's do the training!

Don't panic if the loss curve goes wild. The two networks are competing for the loss curve to go different directions, so virtually anything can happen. If your code is correct, the generated samples should have a high variety.

Do NOT change the number of epochs, learning rate, or batch size. If you're using Google Colab, the batch size will not be an issue during training.

In [ ]:
set_seed(42)

dcgan = DCGAN()
dcgan.train(train_samples)
torch.save(dcgan.state_dict(), "dcgan.pt")
Start training ...
Iteration 100/9750: dis loss = 0.0242, gen loss = 4.7371
Iteration 200/9750: dis loss = 0.1963, gen loss = 4.2365
Iteration 300/9750: dis loss = 0.1831, gen loss = 3.0957
Iteration 400/9750: dis loss = 0.3073, gen loss = 2.5033
Iteration 500/9750: dis loss = 0.7252, gen loss = 1.9279
Iteration 600/9750: dis loss = 0.6413, gen loss = 2.1152
Iteration 700/9750: dis loss = 0.7381, gen loss = 1.2792
Iteration 800/9750: dis loss = 0.6322, gen loss = 1.8668
Iteration 900/9750: dis loss = 0.8249, gen loss = 1.5181
Iteration 1000/9750: dis loss = 0.8457, gen loss = 1.3573
Iteration 1100/9750: dis loss = 0.7046, gen loss = 2.0542
Iteration 1200/9750: dis loss = 0.6909, gen loss = 1.7588
Iteration 1300/9750: dis loss = 0.6322, gen loss = 2.8097
Iteration 1400/9750: dis loss = 1.0496, gen loss = 1.0888
Iteration 1500/9750: dis loss = 1.2120, gen loss = 2.2414
Iteration 1600/9750: dis loss = 0.7388, gen loss = 1.4633
Iteration 1700/9750: dis loss = 1.2186, gen loss = 4.7139
Iteration 1800/9750: dis loss = 0.6511, gen loss = 1.3907
Iteration 1900/9750: dis loss = 0.8867, gen loss = 1.1356
Iteration 2000/9750: dis loss = 0.6245, gen loss = 2.0864
Iteration 2100/9750: dis loss = 0.6485, gen loss = 2.2099
Iteration 2200/9750: dis loss = 0.8509, gen loss = 1.0685
Iteration 2300/9750: dis loss = 0.6576, gen loss = 2.3609
Iteration 2400/9750: dis loss = 0.5921, gen loss = 1.1232
Iteration 2500/9750: dis loss = 0.6502, gen loss = 1.4605
Iteration 2600/9750: dis loss = 0.6544, gen loss = 1.9450
Iteration 2700/9750: dis loss = 0.6432, gen loss = 1.7844
Iteration 2800/9750: dis loss = 0.7671, gen loss = 2.1639
Iteration 2900/9750: dis loss = 0.7231, gen loss = 2.1439
Iteration 3000/9750: dis loss = 0.4755, gen loss = 1.5791
Iteration 3100/9750: dis loss = 0.6820, gen loss = 2.3492
Iteration 3200/9750: dis loss = 0.6313, gen loss = 2.7131
Iteration 3300/9750: dis loss = 1.0817, gen loss = 0.5598
Iteration 3400/9750: dis loss = 0.8664, gen loss = 0.8195
Iteration 3500/9750: dis loss = 0.5589, gen loss = 1.4254
Iteration 3600/9750: dis loss = 1.0048, gen loss = 3.0033
Iteration 3700/9750: dis loss = 0.6441, gen loss = 1.9350
Iteration 3800/9750: dis loss = 0.6687, gen loss = 1.8029
Iteration 3900/9750: dis loss = 0.6432, gen loss = 2.2945
Iteration 4000/9750: dis loss = 0.8771, gen loss = 1.3276
Iteration 4100/9750: dis loss = 0.7499, gen loss = 1.1601
Iteration 4200/9750: dis loss = 0.8214, gen loss = 1.6256
Iteration 4300/9750: dis loss = 0.6122, gen loss = 1.3319
Iteration 4400/9750: dis loss = 0.7781, gen loss = 1.3181
Iteration 4500/9750: dis loss = 0.6645, gen loss = 1.3263
Iteration 4600/9750: dis loss = 0.5925, gen loss = 2.2705
Iteration 4700/9750: dis loss = 0.7422, gen loss = 1.9046
Iteration 4800/9750: dis loss = 0.9329, gen loss = 1.1878
Iteration 4900/9750: dis loss = 0.6349, gen loss = 1.8777
Iteration 5000/9750: dis loss = 0.7414, gen loss = 1.5771
Iteration 5100/9750: dis loss = 1.0029, gen loss = 1.3782
Iteration 5200/9750: dis loss = 0.7696, gen loss = 3.2044
Iteration 5300/9750: dis loss = 0.7195, gen loss = 0.8833
Iteration 5400/9750: dis loss = 0.8012, gen loss = 1.1361
Iteration 5500/9750: dis loss = 0.6462, gen loss = 1.1582
Iteration 5600/9750: dis loss = 0.6338, gen loss = 1.7607
Iteration 5700/9750: dis loss = 0.8577, gen loss = 1.3380
Iteration 5800/9750: dis loss = 0.5926, gen loss = 1.1607
Iteration 5900/9750: dis loss = 0.7722, gen loss = 0.9965
Iteration 6000/9750: dis loss = 0.7009, gen loss = 1.9003
Iteration 6100/9750: dis loss = 0.7345, gen loss = 2.2285
Iteration 6200/9750: dis loss = 0.9916, gen loss = 2.6323
Iteration 6300/9750: dis loss = 0.8046, gen loss = 0.6664
Iteration 6400/9750: dis loss = 0.6828, gen loss = 1.6762
Iteration 6500/9750: dis loss = 0.6719, gen loss = 2.1459
Iteration 6600/9750: dis loss = 0.9165, gen loss = 0.9933
Iteration 6700/9750: dis loss = 0.8604, gen loss = 1.3723
Iteration 6800/9750: dis loss = 0.7405, gen loss = 1.0229
Iteration 6900/9750: dis loss = 0.6814, gen loss = 1.9916
Iteration 7000/9750: dis loss = 0.7313, gen loss = 1.2672
Iteration 7100/9750: dis loss = 0.6307, gen loss = 1.6243
Iteration 7200/9750: dis loss = 0.5868, gen loss = 0.9988
Iteration 7300/9750: dis loss = 0.7052, gen loss = 1.3433
Iteration 7400/9750: dis loss = 0.6030, gen loss = 2.2972
Iteration 7500/9750: dis loss = 0.6712, gen loss = 2.5240
Iteration 7600/9750: dis loss = 0.6221, gen loss = 1.7364
Iteration 7700/9750: dis loss = 0.7302, gen loss = 2.0268
Iteration 7800/9750: dis loss = 0.5484, gen loss = 1.4585
Iteration 7900/9750: dis loss = 0.8659, gen loss = 2.0835
Iteration 8000/9750: dis loss = 0.6011, gen loss = 1.2096
Iteration 8100/9750: dis loss = 0.8000, gen loss = 0.5239
Iteration 8200/9750: dis loss = 0.9386, gen loss = 2.5513
Iteration 8300/9750: dis loss = 0.6625, gen loss = 1.0007
Iteration 8400/9750: dis loss = 0.6534, gen loss = 1.3538
Iteration 8500/9750: dis loss = 0.6206, gen loss = 1.9188
Iteration 8600/9750: dis loss = 0.6297, gen loss = 1.4937
Iteration 8700/9750: dis loss = 0.6720, gen loss = 1.9332
Iteration 8800/9750: dis loss = 0.7953, gen loss = 1.3757
Iteration 8900/9750: dis loss = 1.3104, gen loss = 2.8429
Iteration 9000/9750: dis loss = 0.7458, gen loss = 1.8146
Iteration 9100/9750: dis loss = 0.8443, gen loss = 1.3797
Iteration 9200/9750: dis loss = 0.5252, gen loss = 1.4908
Iteration 9300/9750: dis loss = 0.5969, gen loss = 2.1540
Iteration 9400/9750: dis loss = 0.5416, gen loss = 1.7929
Iteration 9500/9750: dis loss = 0.7410, gen loss = 1.6499
Iteration 9600/9750: dis loss = 0.3404, gen loss = 2.9867
Iteration 9700/9750: dis loss = 0.6182, gen loss = 1.8015
... Done!

Problem 2-2: The Batch Normalization dilemma (4 pts)¶

Here are two questions related to the use of Batch Normalization in GANs. Q1 below will not be graded and the answer is provided. But you should attempt to solve it before looking at the answer.

Q2 will be graded.¶


Q1: We made separate batches for real samples and fake samples when training the discriminator. Is this just an arbitrary design decision made by the inventor that later becomes the common practice, or is it critical to the correctness of the algorithm? [0 pt]

Answer to Q1: When we are training the generator, the input batch to the discriminator will always consist of only fake samples. If we separate real and fake batches when training the discriminator, then the fake samples are normalized in the same way when we are training the discriminator and when we are training the generator. If we mix real and fake samples in the same batch when training the discriminator, then the fake samples are not normalized in the same way when we train the two networks, which causes the generator to fail to learn the correct distribution.

Q2: Look at the construction of the discriminator carefully. You will find that between dis_conv1 and dis_lrelu1 there is no batch normalization. This is not a mistake. What could go wrong if there were a batch normalization layer there? Why do you think that omitting this batch normalization layer solves the problem practically if not theoretically? [3 pt]

Please provide your answer to Q2: If there was a batch normalization in the first layer, then it might hinder the performance. This might be because if you normalize the input distribution, the difference between the distribution betwewen real and fake data can be partially masked. This might make the discriminiator have a harder time differentiating the real and the fake. By not using the batch normalization, it will make it easier for the discriminator to capture the difference between the real and fake data distribution, improving the performance of the GAN.


Takeaway from this problem: always excercise extreme caution when using batch normalization in your network!

For further info (optional): you can read this paper to find out more about why Batch Normalization might be bad for your GANs: On the Effects of Batch and Weight Normalization in Generative Adversarial Networks


Problem 2-3: What about other normalization methods for GAN? (4 pts)¶

Spectral norm is a way of stabilizing the GAN training of discriminator. Please add the embedded spectral norm function in Pytorch to the Discriminator class below in order to test its effects. (see link: https://pytorch.org/docs/stable/generated/torch.nn.utils.spectral_norm.html)

In [ ]:
class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        ################################################################################
        # Prob 2-3:                                                                    #
        # adding spectral norm to the discriminator                                    #
        ################################################################################
        self.downsample = nn.Sequential(
            nn.utils.spectral_norm(nn.Conv2d(in_channels=3, out_channels=32, kernel_size=4, stride=2, padding=1, bias=True)),
            nn.LeakyReLU(),
            nn.utils.spectral_norm(nn.Conv2d(in_channels=32, out_channels=64, kernel_size=4, stride=2, padding=1, bias=True)),
            nn.BatchNorm2d(64),
            nn.LeakyReLU(),
            nn.utils.spectral_norm(nn.Conv2d(in_channels=64, out_channels=128, kernel_size=4, stride=2, padding=1, bias=True)),
            nn.BatchNorm2d(128),
            nn.LeakyReLU(),
        )
        ################################################################################
        # END OF YOUR CODE                                                             #
        ################################################################################
        self.fc = nn.Linear(4 * 4 * 128, 1)
    def forward(self, input):
        downsampled_image = self.downsample(input)
        reshaped_for_fc = downsampled_image.reshape((-1, 4 * 4 * 128))
        classification_probs = self.fc(reshaped_for_fc)
        return classification_probs

After adding the spectral norm to the discriminator, redo the training block below to see the effects.

In [ ]:
set_seed(42)

dcgan = DCGAN()
dcgan.train(train_samples)
torch.save(dcgan.state_dict(), "dcgan.pt")
Start training ...
Iteration 100/9750: dis loss = 0.4008, gen loss = 3.2815
Iteration 200/9750: dis loss = 0.1847, gen loss = 3.2941
Iteration 300/9750: dis loss = 0.2434, gen loss = 3.6901
Iteration 400/9750: dis loss = 0.4914, gen loss = 2.3408
Iteration 500/9750: dis loss = 0.7105, gen loss = 2.6949
Iteration 600/9750: dis loss = 0.4409, gen loss = 2.4475
Iteration 700/9750: dis loss = 0.7734, gen loss = 2.4643
Iteration 800/9750: dis loss = 0.6835, gen loss = 1.4125
Iteration 900/9750: dis loss = 1.0732, gen loss = 2.9692
Iteration 1000/9750: dis loss = 0.7200, gen loss = 1.0311
Iteration 1100/9750: dis loss = 0.6040, gen loss = 1.9684
Iteration 1200/9750: dis loss = 0.6041, gen loss = 2.8225
Iteration 1300/9750: dis loss = 1.2394, gen loss = 1.1113
Iteration 1400/9750: dis loss = 0.5149, gen loss = 2.7565
Iteration 1500/9750: dis loss = 0.4713, gen loss = 2.3292
Iteration 1600/9750: dis loss = 0.5577, gen loss = 2.5701
Iteration 1700/9750: dis loss = 0.5773, gen loss = 1.4931
Iteration 1800/9750: dis loss = 0.9438, gen loss = 2.8252
Iteration 1900/9750: dis loss = 0.6160, gen loss = 1.4538
Iteration 2000/9750: dis loss = 0.7081, gen loss = 1.1883
Iteration 2100/9750: dis loss = 0.6822, gen loss = 2.1588
Iteration 2200/9750: dis loss = 0.6661, gen loss = 1.9622
Iteration 2300/9750: dis loss = 0.6604, gen loss = 2.3691
Iteration 2400/9750: dis loss = 0.5759, gen loss = 2.1471
Iteration 2500/9750: dis loss = 0.7622, gen loss = 1.5490
Iteration 2600/9750: dis loss = 0.5783, gen loss = 1.6578
Iteration 2700/9750: dis loss = 0.7096, gen loss = 1.9638
Iteration 2800/9750: dis loss = 0.6118, gen loss = 1.7022
Iteration 2900/9750: dis loss = 0.8075, gen loss = 1.2900
Iteration 3000/9750: dis loss = 0.9000, gen loss = 0.6559
Iteration 3100/9750: dis loss = 0.7396, gen loss = 3.0561
Iteration 3200/9750: dis loss = 0.8268, gen loss = 1.4785
Iteration 3300/9750: dis loss = 0.7691, gen loss = 2.2451
Iteration 3400/9750: dis loss = 0.7069, gen loss = 1.3209
Iteration 3500/9750: dis loss = 0.8842, gen loss = 2.6923
Iteration 3600/9750: dis loss = 0.5223, gen loss = 1.7201
Iteration 3700/9750: dis loss = 0.8958, gen loss = 1.8229
Iteration 3800/9750: dis loss = 0.6256, gen loss = 1.8334
Iteration 3900/9750: dis loss = 0.6970, gen loss = 1.7520
Iteration 4000/9750: dis loss = 0.6502, gen loss = 1.3285
Iteration 4100/9750: dis loss = 0.6627, gen loss = 1.8326
Iteration 4200/9750: dis loss = 0.5819, gen loss = 2.6059
Iteration 4300/9750: dis loss = 0.6667, gen loss = 2.1297
Iteration 4400/9750: dis loss = 0.6649, gen loss = 2.0389
Iteration 4500/9750: dis loss = 0.6354, gen loss = 2.0256
Iteration 4600/9750: dis loss = 0.6818, gen loss = 1.5043
Iteration 4700/9750: dis loss = 0.9688, gen loss = 0.9026
Iteration 4800/9750: dis loss = 0.7747, gen loss = 1.4131
Iteration 4900/9750: dis loss = 0.5679, gen loss = 1.7282
Iteration 5000/9750: dis loss = 0.6229, gen loss = 1.3746
Iteration 5100/9750: dis loss = 0.7182, gen loss = 2.6245
Iteration 5200/9750: dis loss = 0.5057, gen loss = 1.2609
Iteration 5300/9750: dis loss = 0.6317, gen loss = 1.6988
Iteration 5400/9750: dis loss = 0.6762, gen loss = 1.9397
Iteration 5500/9750: dis loss = 0.6861, gen loss = 1.1922
Iteration 5600/9750: dis loss = 0.8390, gen loss = 2.9640
Iteration 5700/9750: dis loss = 0.7057, gen loss = 1.3042
Iteration 5800/9750: dis loss = 0.6549, gen loss = 2.0386
Iteration 5900/9750: dis loss = 0.4774, gen loss = 1.4654
Iteration 6000/9750: dis loss = 0.6534, gen loss = 1.9180
Iteration 6100/9750: dis loss = 0.8208, gen loss = 1.8732
Iteration 6200/9750: dis loss = 0.5947, gen loss = 1.5159
Iteration 6300/9750: dis loss = 0.6228, gen loss = 2.0762
Iteration 6400/9750: dis loss = 0.5683, gen loss = 1.6108
Iteration 6500/9750: dis loss = 0.5484, gen loss = 2.2216
Iteration 6600/9750: dis loss = 0.7523, gen loss = 1.6811
Iteration 6700/9750: dis loss = 0.6939, gen loss = 2.3649
Iteration 6800/9750: dis loss = 0.7799, gen loss = 1.5089
Iteration 6900/9750: dis loss = 0.5649, gen loss = 1.6141
Iteration 7000/9750: dis loss = 0.6770, gen loss = 1.5322
Iteration 7100/9750: dis loss = 0.6434, gen loss = 2.5814
Iteration 7200/9750: dis loss = 0.6033, gen loss = 1.1357
Iteration 7300/9750: dis loss = 0.5414, gen loss = 2.5772
Iteration 7400/9750: dis loss = 0.6446, gen loss = 2.5061
Iteration 7500/9750: dis loss = 0.6004, gen loss = 2.8679
Iteration 7600/9750: dis loss = 0.5725, gen loss = 2.2049
Iteration 7700/9750: dis loss = 0.5115, gen loss = 1.4609
Iteration 7800/9750: dis loss = 0.8966, gen loss = 0.7793
Iteration 7900/9750: dis loss = 0.6216, gen loss = 1.7505
Iteration 8000/9750: dis loss = 0.4766, gen loss = 2.0940
Iteration 8100/9750: dis loss = 0.4452, gen loss = 2.4923
Iteration 8200/9750: dis loss = 0.8749, gen loss = 1.3514
Iteration 8300/9750: dis loss = 0.6431, gen loss = 1.8592
Iteration 8400/9750: dis loss = 0.6533, gen loss = 1.9075
Iteration 8500/9750: dis loss = 0.5335, gen loss = 1.0163
Iteration 8600/9750: dis loss = 0.3980, gen loss = 1.9965
Iteration 8700/9750: dis loss = 0.6359, gen loss = 0.8813
Iteration 8800/9750: dis loss = 0.5798, gen loss = 2.9754
Iteration 8900/9750: dis loss = 0.4520, gen loss = 1.4412
Iteration 9000/9750: dis loss = 0.4780, gen loss = 1.7296
Iteration 9100/9750: dis loss = 0.7289, gen loss = 1.0264
Iteration 9200/9750: dis loss = 0.5206, gen loss = 2.2639
Iteration 9300/9750: dis loss = 0.4151, gen loss = 1.8191
Iteration 9400/9750: dis loss = 0.3716, gen loss = 1.9435
Iteration 9500/9750: dis loss = 0.6760, gen loss = 1.8032
Iteration 9600/9750: dis loss = 0.7037, gen loss = 0.9099
Iteration 9700/9750: dis loss = 0.4501, gen loss = 2.6468
... Done!

Problem 2-4: Activation Maximization (12 pts)¶

Activation Maximization is a visualization technique to see what a particular neuron has learned, by finding the input that maximizes the activation of that neuron. Here we use methods similar to Synthesizing the preferred inputs for neurons in neural networks via deep generator networks.

In short, what we want to do is to find the samples that the discriminator considers most real, among all possible outputs of the generator, which is to say, we want to find the codes (i.e. a point in the input space of the generator) from which the generated images, if labelled as real, would minimize the classification loss of the discriminator:

$$\min_{z} L(D_{\theta}(G_{\phi}(z)), 1)$$

Compare this to the objective when we were training the generator:

$$\min_{\phi} \mathrm{E}_{z \sim q(z)}[L(D_{\theta}(G_{\phi}(z)), 1)]$$

The function to minimize is the same, with the difference being that when training the network we fix a set of input data and find the optimal model parameters, while in activation maximization we fix the model parameters and find the optimal input.

So, similar to the training, we use gradient descent to solve for the optimal input. Starting from a random code (latent vector) drawn from a standard normal distribution, we perform a fixed step of Adam optimization algorithm on the code (latent vector).

The batch normalization layers should work in evaluation mode.

We provide the code for this part, as a reference for solving the next part. You may want to go back to the code above and check the actmax function and figure out what it's doing:

In [ ]:
set_seed(241)

dcgan = DCGAN()
dcgan.load_state_dict(torch.load("dcgan.pt", map_location=device))

actmax_results = dcgan.actmax(np.random.normal(size=(64, dcgan.code_size)))
fig = plt.figure(figsize = (8, 8))   
ax1 = plt.subplot(111)
ax1.imshow(make_grid(actmax_results, padding=1, normalize=True).numpy().transpose((1, 2, 0)))
plt.show()

The output should have less variety than those generated from random code, but look realisitic.

A similar technique can be used to reconstruct a test sample, that is, to find the code that most closely approximates the test sample. To achieve this, we only need to change the loss function from discriminator's loss to the squared L2-distance between the generated image and the target image:

$$\min_{z} \left|\left|G_{\phi}(z)-x\right|\right|_2^2$$

This time, we always start from a zero vector.

For this part, you need to complete code blocks marked with "Prob 2-4" above. Then run the following block.¶

You need to achieve a reconstruction loss < 0.145. Do NOT modify anything outside of the blocks marked for you to fill in.

In [ ]:
dcgan = DCGAN()
dcgan.load_state_dict(torch.load("dcgan.pt", map_location=device))

avg_loss, reconstructions = dcgan.reconstruct(test_samples[0:64])
print('average reconstruction loss = {0:.4f}'.format(avg_loss))
fig = plt.figure(figsize = (8, 8))   
ax1 = plt.subplot(111)
ax1.imshow(make_grid(torch.from_numpy(test_samples[0:64]), padding=1).numpy().transpose((1, 2, 0)))
plt.show()
fig = plt.figure(figsize = (8, 8))   
ax1 = plt.subplot(111)
ax1.imshow(make_grid(reconstructions, padding=1, normalize=True).numpy().transpose((1, 2, 0)))
plt.show()
average reconstruction loss = 0.0140

Submission Instruction¶

See the pinned Piazza post for detailed instruction.

In [ ]: